<?php

namespace backend\components\adapter;

use Yii;
use yii\helpers\FileHelper;
use yii\helpers\VarDumper;
use yii\base\ErrorException;
use backend\models\Clients;
use backend\components\Installer;
use backend\components\InstallerAdapter;
use backend\modules\admin\models\Menu;
use backend\modules\admin\models\Route;
use backend\modules\admin\components\Helper;
use backend\modules\admin\components\Configs;

/**
 * Module installer
 *
 * @author loong
 */
class ModuleAdapter extends InstallerAdapter
{
    public $clients;

    public function init()
    {
        $this->clients = Clients::getList();
        parent::init();
    }

    /**
     * {@inheritdoc}
     */
    public function getElement($element = null)
    {
        $element = parent::getElement();
        if (strpos($element, 'mod_') !== 0) {
            $element = 'mod_' . $element;
        }
        return $element;
    }

    /**
     * {@inheritdoc}
     */
    protected function setupInstallPaths()
    {
        $moduleId = trim(strtolower((string)$this->manifest->element ?: $this->name));
        $clientName = [];
        foreach ($this->clients as $client) {
            $this->parent->setPath('extension_' . $client, Yii::getAlias('@' . $client) . '/modules/' . $moduleId);
            $clientName[] = $this->manifest->$client->getName();
        }

        $currentClient = (string)$this->manifest->attributes()->client;

        $this->parent->setPath('extension_root', $this->parent->getPath('extension_' . $currentClient));

        if (!in_array('backend', array_filter($clientName))) {
            throw new \RuntimeException(Yii::t('installer', 'ERROR_MOD_INSTALL_BACKEND_ELEMENT'));
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function copyBaseFiles()
    {
        foreach ($this->clients as $client) {
            if ($this->manifest->$client->files) {
                if ($this->route === 'update') {
                    $result = $this->parent->parseFiles($this->manifest->$client->files, $client, $this->oldFiles);
                } else {
                    $result = $this->parent->parseFiles($this->manifest->$client->files, $client);
                }

                if ($result === false) {
                    throw new \RuntimeException(Yii::t('installer', 'ERROR_MOD_INSTALL_FAIL_COPY_FILE'));
                }
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function parseOptionalTags()
    {
        foreach ($this->clients as $client) {
            $messages = $this->manifest->$client->messages;
            if ($messages !== null) {
                $this->parent->parseMessages($this->manifest->$client->messages, $client);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function storeExtension($deleteExisting = false)
    {
        // If we are told to delete existing extension entries then do so.
        if ($deleteExisting) {
            $extensions = $this->extension->findAll([
                'name' => $this->name,
                'type' => $this->type,
                'element' => $this->element
            ]);

            if (!empty($extensions)) {
                foreach ($extensions as $extension) {
                    $extension->delete();
                }
            }
        }

        // If there is not already a row, generate a heap of defaults
        if (is_null($this->currentExtensionId)) {
            $this->extension->folder = '';
            $this->extension->enabled = 1;
            $this->extension->protected = 0;
            $this->extension->params = $this->parent->getParams();
            $this->extension->status = 1;
        } else {
            $this->extension = $this->currentExtensionId;
        }
        $currentClient = strtolower(trim((string)$this->manifest->attributes()->client));
        $this->extension->name = $this->name;
        $this->extension->type = $this->type;
        $this->extension->element = $this->element;
        $this->extension->client_id = $this->parent->getClientId($currentClient);
        $this->extension->manifest_cache = $this->parent->generateManifestCache();

        $couldSave = $this->extension->save();

        if (!$couldSave && $deleteExisting) {
            $errorStr = '';
            foreach ($this->extension->errors as $errors) {
                foreach ($errors as $error) {
                    $errorStr .= '<br>&emsp;&emsp;' . $error;
                }
            }

            $errorMessage = Yii::t('installer', 'ABORT_MOD_INSTALL_ROLLBACK', $errorStr);
            throw new \RuntimeException($errorMessage);
        }
        if (!$couldSave && !$deleteExisting) {
            $this->storeExtension(true);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function finaliseInstall()
    {
        $moduleId = trim(strtolower((string)$this->manifest->element ?: $this->name));
        $this->_writeModuleConfig($moduleId);
        $this->_writeExtensionConfig($moduleId);
        $this->_writeParamsConfig();
    }

    /**
     * 写入模块配置
     * @param string $moduleId 模块名
     */
    protected function _writeModuleConfig($moduleId)
    {
        $rootPath = Yii::getAlias('@root') . DIRECTORY_SEPARATOR;
        foreach ($this->clients as $client) {
            $modulesConfigPath = Yii::getAlias('@' . $client . '/config/modules.php');
            $oldModuleConfigs = include $modulesConfigPath;
            if (boolval($this->manifest->$client)) {
                if (boolval($this->manifest->$client->moduleConfigs)) {
                    $configs = \backend\components\VarDumper::xmlExport($this->manifest->$client->moduleConfigs, 1);
                    if (in_array($moduleId, array_keys($oldModuleConfigs))) {
                        unset($oldModuleConfigs[$moduleId]);
                    }
                    if (empty($oldModuleConfigs)) {
                        $content = '<?php' . PHP_EOL . PHP_EOL . 'return [' . PHP_EOL;
                        $content .= '    \'' . $moduleId . '\'' . ' => ' . $configs . ',' . PHP_EOL . '];' . PHP_EOL;
                    } else {
                        $oldContent = VarDumper::export($oldModuleConfigs);
                        $search = [PHP_EOL . ']', '\\\\'];
                        $replace = [PHP_EOL . '    \'' . $moduleId . '\' => ' . $configs . ',' . PHP_EOL . ']', '\\'];
                        $export = str_replace($search, $replace, $oldContent);
                        $content = '<?php' . PHP_EOL . PHP_EOL . 'return ' . $export . ';' . PHP_EOL;
                    }
                } else {
                    $modulePath = $this->parent->getPath('extension_' . $client) . DIRECTORY_SEPARATOR . 'Module';
                    $className = str_replace('/', '\\', substr($modulePath, strlen($rootPath)));
                    $moduleConfig[$moduleId]['class'] = $className;
                    $modulesConfigPath = Yii::getAlias('@' . $client) . '/config/modules.php';
                    $oldModuleConfigs = include $modulesConfigPath;
                    foreach ($oldModuleConfigs as $mk => $mv) {
                        if ($mv['class'] == $className) {
                            unset($oldModuleConfigs[$mk]);
                        }
                    }
                    $modulesConfig = array_merge($oldModuleConfigs, $moduleConfig);
                    $export = str_replace('\\\\', '\\', VarDumper::export($modulesConfig));
                    $content = '<?php' . PHP_EOL . PHP_EOL . 'return ' . $export . ';' . PHP_EOL;
                }
                try {
                    file_put_contents($modulesConfigPath, $content, LOCK_EX);
                } catch (ErrorException $e) {
                    throw new \RuntimeException($e->getMessage());
                }
            }
        }

        if (boolval($this->manifest->backend)) {
            $modulePath = $this->parent->getPath('extension_backend') . DIRECTORY_SEPARATOR . 'Module';
            $className = str_replace('/', '\\', substr($modulePath, strlen($rootPath)));
            if (!$this->_createBackendMenus($this->extension, $className)) {
                Yii::warning(Yii::t('installer', 'ABORT_MOD_BUILD_BACKEND_MENUS_FAILED'));
            }
        }
    }

    /**
     * 写入扩展配置
     * @param string $moduleId 模块名
     * @param array $clients 应用列表
     */
    protected function _writeExtensionConfig($moduleId)
    {
        foreach ($this->clients as $client) {
            $extensionConfigPath = Yii::getAlias('@' . $client . '/config/extension.php');
            $oldExtensionConfigs = include $extensionConfigPath;
            if ($this->manifest->$client->extensionConfigs) {
                $configs = \backend\components\VarDumper::xmlExport($this->manifest->$client->extensionConfigs);
                $content = '$' . $moduleId . ' = ' . $configs . ';';
                if (empty($oldExtensionConfigs)) {
                    $content = '<?php' . PHP_EOL . PHP_EOL . $content . PHP_EOL . PHP_EOL . 'return array_merge($' . $moduleId . ');' . PHP_EOL;
                } else {
                    $oldContent = file_get_contents($extensionConfigPath);
                    $pattern = '/\n\$' . $moduleId . ' = \[(.*?)\];/ims';
                    preg_match_all($pattern, $oldContent, $matches);
                    $search = PHP_EOL . 'return array_merge(';
                    $replace = $content . PHP_EOL . PHP_EOL . 'return array_merge(';
                    if (count($matches[0])) {
                        $oldContent = preg_replace($pattern, '', $oldContent);
                        $content = str_replace($search, $replace, trim($oldContent)) . PHP_EOL;
                    } else {
                        $content = str_replace($search, PHP_EOL . $replace, trim($oldContent));
                        $content = substr($content, 0, strrpos($content, ');')) . ', $' . $moduleId . ');' . PHP_EOL;
                    }
                }

                try {
                    file_put_contents($extensionConfigPath, $content, LOCK_EX);
                } catch (ErrorException $e) {
                    throw new \RuntimeException($e->getMessage());
                }
            }
        }
    }

    /**
     * 写入参数配置
     * @param array $clients 应用列表
     */
    protected function _writeParamsConfig()
    {
        foreach ($this->clients as $client) {
            $configPath = Yii::getAlias('@' . $client . '/config/params.php');
            $oldConfigs = include $configPath;
            if ($this->manifest->$client->paramConfigs) {
                $configs = \backend\components\VarDumper::xmlExport($this->manifest->$client->paramConfigs, 0, false);

                $items = $this->manifest->$client->paramConfigs->item;
                foreach ($items as $item) {
                    if (in_array((string)$item->attributes()->key, array_keys($oldConfigs))) {
                        unset($oldConfigs[(string)$item->attributes()->key]);
                    }
                }

                if (empty($oldConfigs)) {
                    $content = '<?php' . PHP_EOL . PHP_EOL . 'return [' . $configs . PHP_EOL . '];' . PHP_EOL;
                } else {
                    $oldContent = VarDumper::export($oldConfigs);
                    $search = [PHP_EOL . ']', '\\\\'];
                    $replace = [$configs . PHP_EOL . ']', '\\'];
                    $export = str_replace($search, $replace, $oldContent);
                    $content = '<?php' . PHP_EOL . PHP_EOL . 'return ' . $export . ';' . PHP_EOL;
                }

                try {
                    file_put_contents($configPath, $content, LOCK_EX);
                } catch (ErrorException $e) {
                    throw new \RuntimeException($e->getMessage());
                }
            }
        }
    }

    /**
     * 创建后台菜单
     * @param object $extension 模块对象
     * @param string $className 模块类名
     * @return bool
     * @throws Yii\base\InvalidConfigException
     */
    protected function _createBackendMenus($extension, $className = '')
    {
        $menuElement = $this->getManifest()->menu;
        $moduleId = trim(strtolower((string)$this->manifest->element ?: $this->name));
        $this->_updateRoute($moduleId);
        $langCat = $this->getManifest()->backend->messages->attributes()->main ?? $this->getElement();
        if (!$menuElement) {
            Yii::$app->setModule($moduleId, $className);
            $module = Yii::$app->getModule($moduleId);
            $routeArr = explode('/', trim($module->defaultRoute, '/'));
            if (count($routeArr) === 1) {
                $defaultRoute = $routeArr[0] . '/index';
            } else {
                $defaultRoute = $routeArr[0] . '/' . $routeArr[1];
            }
            $defaultRoute = '@' . Yii::$app->id . '/' . $moduleId . '/' . $defaultRoute;

            if ($this->route === 'update') {
                $menus = Menu::find()->where(['menutype' => 'main', 'type' => 'module', 'level' => 1]);
                if ($extension->protected === 1) {
                    $menus->andWhere(['<>', 'parent', 10]);
                } else {
                    $menus->andWhere(['parent' => 10]);
                }
                foreach ($menus->all() as $item) {
                    if (!is_null($item->data)) {
                        $data = json_decode($item->data, true);
                        if (isset($data['module-id']) && $extension->id === $data['module-id']) {
                            $menu = $item;
                            break;
                        }
                    }
                }
            } else {
                $menu = new Menu();
                $menu->alias = '';
                $menu->menutype = 'main';
                $menu->type = 'module';
                $menu->level = 1;
                $menu->parent = 10;
            }

            $menu->name = $this->name;
            $menu->published = 1;
            $menu->route = $defaultRoute;
            $menu->language = '*';
            $menu->data = json_encode(['module-id' => $extension->id, 'lang-cat' => (string)$langCat]);

            if ($menu->save()) {
                Helper::invalidate();
                $menu->setLftRgt();
                $this->parent->pushStep(['type' => 'menu', 'id' => $menu->id]);
                return true;
            } else {
                return false;
            }
        }
        if (in_array((string)$menuElement['hidden'], ['true', 'hidden'])) {
            return true;
        }
        if ($menuElement) {
            if ($this->route === 'update') {
                $menus = Menu::find()->where(['menutype' => 'main', 'type' => 'module', 'level' => 1]);
                if ($extension->protected === 1) {
                    $menus->andWhere(['<>', 'parent', 10]);
                } else {
                    $menus->andWhere(['parent' => 10]);
                }
                $menus->all();
                foreach ($menus as $item) {
                    if (!is_null($item->data)) {
                        $data = json_decode($item->data, true);
                        if ($extension->id === $data['module-id']) {
                            $menu = $item;
                            break;
                        }
                    }
                }
            } else {
                $menu = new Menu();
                $menu->alias = '';
                $menu->menutype = 'main';
                $menu->type = 'module';
                $menu->level = 1;
                $menu->parent = 10;
            }

            $moduleId = trim(strtolower((string)$this->manifest->element ?: $this->name));
            $route = (string)$menuElement->attributes()->route ?: $moduleId;

            $menu->name = trim((string)$menuElement);
            $menu->published = 1;
            $menu->route = '@' . Yii::$app->id . '/' . $route;
            $menu->language = '*';
            $menu->data = json_encode(['module-id' => $extension->id, 'lang-cat' => (string)$langCat]);

            if ($menu->save()) {
                Helper::invalidate();
                $menu->setLftRgt();
                $this->parent->pushStep(['type' => 'menu', 'id' => $this->extension->id]);
                $subMenuElement = $this->getManifest()->submenu;
                if (!$subMenuElement) {
                    return true;
                }
                $parentId = $menu->id;
                foreach ($subMenuElement->menu as $child) {
                    if ($this->route === 'update') {
                        $menus = Menu::find()
                            ->where(['menutype' => 'main', 'type' => 'module', 'level' => 2, 'parent' => $parentId])
                            ->all();
                        foreach ($menus as $item) {
                            if (!is_null($item->data)) {
                                $data = json_decode($item->data, true);
                                if ($extension->id === $data['module-id']) {
                                    $menu = $item;
                                    break;
                                }
                            }
                        }
                    } else {
                        $menu = new Menu();
                        $menu->alias = '';
                        $menu->menutype = 'main';
                        $menu->type = 'module';
                        $menu->level = 2;
                        $menu->parent = $parentId;
                    }
                    $menu->name = trim((string)$child);
                    $menu->menutype = 'main';
                    $menu->published = 1;
                    $menu->route = '@' . Yii::$app->id . '/' . (string)$child->attributes()->route;
                    $menu->language = '*';
                    $menu->data = json_encode(['module-id' => $extension->id, 'lang-cat' => (string)$langCat]);
                    $menu->save();
                }
                Helper::invalidate();
                (new Menu())->setLftRgt();
                $this->parent->pushStep(['type' => 'menu', 'id' => $extension->id]);
                return true;
            } else {
                return false;
            }
        }
    }

    /**
     * @param $moduleId
     * @throws Yii\base\InvalidConfigException
     */
    protected function _updateRoute($moduleId)
    {
        foreach ($this->clients as $client) {
            if (boolval($this->manifest->$client)) {
                $routeModel = new Route();
                $routeModel->update($moduleId, $client);
            }
        }
    }

    /**
     * 卸载模块
     * @param $extension
     * @return bool
     * @throws ErrorException
     * @throws \Throwable
     * @throws \yii\db\StaleObjectException
     */
    public function uninstall($extension)
    {
        $session = Yii::$app->session;
        if ($extension->protected) {
            $extensionName = Yii::t($extension->manifest_cache['langCat'], $extension->name);
            $msg = Yii::t('installer', 'ERROR_MOD_UNINSTALL_WARN_CORE_MODULE', $extensionName);
            $session->addFlash('warning', $msg);
            return false;
        }

        if ($extension->package_id) {
            $extensionName = Yii::t($extension->manifest_cache['langCat'], $extension->name);
            $session->addFlash('warning', Yii::t('installer', 'ERROR_CANNOT_UNINSTALL_CHILD_OF_PACKAGE', $extensionName));
            return false;
        }

        $moduleId = substr($extension->element, 4);
        foreach ($this->clients as $item) {
            $path = Yii::getAlias('@' . $item . '/modules/' . $moduleId);
            $this->parent->setPath('extension_' . $item, $path);
        }

        $this->parent->setPath('source', $this->parent->getPath('extension_backend'));

        $this->parent->findManifest();

        $this->manifest = $this->parent->manifest;

        if (!$this->manifest) {
            foreach ($this->clients as $client) {
                FileHelper::removeDirectory($this->parent->getPath('extension_' . $client));
            }
            $this->_removeModuleConfig($moduleId);
            $this->_removeBackendMenus($extension->id);
            $session->addFlash('warning', Yii::t('installer', 'ERROR_MOD_UNINSTALL_REMOVE_MANUALLY'));
            return false;
        }

        $retval = true;

        try {
            $this->parseQueries();
        } catch (\RuntimeException $re) {
            $session->addFlash('warning', $re->getMessage());
            $retval = false;
        }

        $this->_removeBackendMenus($extension->id);

        $manifest = $this->manifest;

        foreach ($this->clients as $item) {
            if ($this->manifest->$item) {
                $messagePath = Yii::getAlias('@' . $item . '/messages/');
                $message = $manifest->$item->messages->message;
                $messageFile = $messagePath . $message->attributes()->tag . '/' . (string)$message;
                if (is_file($messageFile)) {
                    FileHelper::unlink($messageFile);
                }
                FileHelper::removeDirectory($this->parent->getPath('extension_' . $item));
            }
        }
        $extension->delete();

        // 移除模块配置
        $this->_removeModuleConfig($moduleId);
        return $retval;
    }

    /**
     * 移除模块的后台菜单
     * @param int $id 模块扩展ID
     * @return bool
     * @throws \Throwable
     * @throws \yii\db\StaleObjectException
     */
    protected function _removeBackendMenus($id)
    {
        $manMenus = \common\components\Helper::getMenus('main');
        $mids = [];
        $result = true;
        foreach ($manMenus as $menu) {
            $data = json_decode($menu['data'], true);
            if (isset($data['module-id']) && $data['module-id'] == $id) {
                $mids[] = $menu['id'];
                $menuModel = Menu::findOne($menu['id']);
                if (!$menuModel->delete()) {
                    Yii::$app->session->addFlash('error', $this->parent->errorsToString($menuModel->getErrors()));
                    $result = false;
                }
            }
        }
        Helper::invalidate();
        (new Menu())->setLftRgt();
        // 删除路由
        $moduleId = trim(strtolower((string)($this->manifest->element ?: $this->manifest->name)));
        $routeModel = new Route();
        $routes = $routeModel->getAppRoutes($moduleId);
        array_walk($routes, function (&$value) {
            $value = '@' . Yii::$app->id . $value;
        });
        $routeModel->remove($routes);

        return $result;
    }

    /**
     * 移除模块配置
     * @param $moduleId 模块扩展ID
     */
    protected function _removeModuleConfig($moduleId)
    {
        $modulesConfigPath = Yii::getAlias('@backend/config/modules.php');
        if (is_file($modulesConfigPath)) {
            $oldModuleConfig = include $modulesConfigPath;
            $newModuleConfig = [];
            foreach ($oldModuleConfig as $mk => $mv) {
                if ($mk !== $moduleId) {
                    $newModuleConfig[$mk] = $mv;
                }
            }
            if ($newModuleConfig !== $oldModuleConfig) {
                $export = str_replace('\\\\', '\\', VarDumper::export($newModuleConfig));
                $content = '<?php' . PHP_EOL . PHP_EOL . 'return ' . $export . ';' . PHP_EOL;
                file_put_contents($modulesConfigPath, $content, LOCK_EX);
            }
        }
    }

    /**
     * Method to setup the update routine for the adapter
     * @return void
     */
    protected function setupUpdates()
    {
        $tmpInstaller = new Installer();
        foreach ($this->clients as $client) {
            $tmpInstaller->setPath('source', $this->parent->getPath('extension_' . $client));
            if ($tmpInstaller->findManifest()) {
                $oldManifest = $tmpInstaller->getManifest();
                if ($oldManifest) {
                    $this->set('oldFiles', $oldManifest->{$client}->files);
                    //$this->set('oldClient', $client);
                    break;
                }
            }
        }
    }
}
